Over-the-Air NVIDIA Jetson - RAUC Update Flow and Custom Bootloader
RAUC on NVIDIA Jetson: Update Flow and Custom Bootloader Backend
This page documents the complete signed update flow after the base RAUC integration is already working.
It covers:
- Generating signing keys
- Installing the correct CA into the image
- Creating a RAUC bundle recipe
- Installing signed bundles on target
- Manual A/B switching via extlinux
- Replacing noop with a custom bootloader backend
Assumptions
This page assumes:
- RAUC is already integrated in the image
- rauc.service starts successfully
- Slot detection already works on target
- root= and rauc.slot= are already present in the kernel cmdline
RAUC Bundle Creation and A/B Update Validation
1. Generating Signing Keys
RAUC requires:
- A CA certificate installed on target
- A signing key + certificate used to sign bundles
The helper script from meta-rauc was used:
bash layers/meta-rauc/scripts/openssl-ca.sh \
layers/meta-tegrademo/recipes-core/rauc/rauc-keys/files
This generated:
- openssl-ca/dev/ca.cert.pem
- openssl-ca/dev/private/development-1.key.pem
- openssl-ca/dev/development-1.cert.pem
Verification:
openssl x509 -in openssl-ca/dev/ca.cert.pem -noout -fingerprint -sha256
2. Installing the Correct CA into the Image
Initially, the target contained the default RAUC test CA:
openssl x509 -in /etc/rauc/ca.cert.pem -noout -subject
Output:
CN=RAUC Test CA
This caused bundle verification failure.
Error Observed
signature verification failed: unable to get local issuer certificate
Root Cause
The bundle was signed with the newly generated CA, but the target still trusted the default RAUC Test CA.
Fix
Copy the generated CA into the BSP layer:
cp openssl-ca/dev/ca.cert.pem \ layers/meta-tegrademo/recipes-core/rauc/rauc-conf/files/ca.cert.pem
Then rebuild and reflash the image.
After reflashing:
openssl x509 -in /etc/rauc/ca.cert.pem -noout -fingerprint -sha256
Fingerprint matched the build CA.
3. Creating the RAUC Bundle Recipe
A custom bundle recipe was created:
meta-tegrademo/recipes-core/rauc/demo-bundle/demo-bundle.bb
Minimal example:
SUMMARY = "Demo RAUC bundle for demo-image-base"
LICENSE = "MIT"
inherit bundle
RAUC_BUNDLE_COMPATIBLE = "jetson-agx-orin"
RAUC_BUNDLE_FORMAT = "plain"
RAUC_BUNDLE_SLOTS = "rootfs"
RAUC_SLOT_rootfs = "demo-image-base"
RAUC_SLOT_rootfs[fstype] = "ext4"
RAUC_SLOT_rootfs[file] = "demo-image-base-${MACHINE}.rootfs.ext4"
RAUC_KEY_FILE = "${TOPDIR}/../openssl-ca/dev/private/development-1.key.pem"
RAUC_CERT_FILE = "${TOPDIR}/../openssl-ca/dev/development-1.cert.pem"
Build:
bitbake demo-bundle
Result:
tmp/deploy/images/<machine>/demo-bundle-<machine>.raucb
4. Installing the Bundle on Target
Copy bundle:
scp demo-bundle-*.raucb root@target:/root/
Install:
rauc install /root/demo-bundle-*.raucb
Successful output:
100% Installing done. Installing succeeded
The image was written to:
/dev/mmcblk0p2 (rootfs.1)
5. Slot State After Installation
Immediately after installation:
rauc status
Output:
- Booted from rootfs.0 (A)
- rootfs.1 marked inactive
- bootloader shown as none (noop backend)
Important:
With bootloader=noop, RAUC does NOT switch slots automatically.
6. Manual Slot Switching (extlinux)
Jetson uses extlinux.
Edit:
/boot/extlinux/extlinux.conf
Original:
APPEND ... root=/dev/mmcblk0p1 rauc.slot=A
Modified:
APPEND ... root=/dev/mmcblk0p2 rauc.slot=B
Reboot the device.
Verification:
findmnt -n -o SOURCE / cat /proc/cmdline | grep rauc.slot
System should now boot from:
/dev/mmcblk0p2 rauc.slot=B
7. Marking the Slot Good
After confirming successful boot:
rauc status mark-good booted
This marks the currently booted slot as valid.
Note:
With noop backend, this does not affect bootloader state, but RAUC internal state is updated.
Implementing a Custom Bootloader Backend for Jetson (extlinux-based)
Motivation
Because bootloader=noop does not allow persistent slot activation, a custom backend was introduced to make slot switching visible to RAUC.
Step 1: Splitting extlinux into A/B Entries
The first requirement was to define independent boot entries for each rootfs.
File modified:
/boot/extlinux/extlinux.conf
Final structure:
# L4TLauncher configuration file generated by OE4T
MENU TITLE L4T boot options
DEFAULT rauc-B
TIMEOUT 30
LABEL rauc-A
MENU LABEL RAUC slot A
LINUX /boot/Image
INITRD /boot/initrd
APPEND ${cbootargs} root=/dev/mmcblk0p1 rauc.slot=A mminit_loglevel=4 console=tty0 console
LABEL rauc-B
MENU LABEL RAUC slot B
LINUX /boot/Image
INITRD /boot/initrd
APPEND ${cbootargs} root=/dev/mmcblk0p2 rauc.slot=B mminit_loglevel=4 console=tty0 console
Key design decisions:
- Each slot has its own LABEL.
- Each slot explicitly defines:
- root=
- rauc.slot=
- DEFAULT determines which slot will boot next.
Step 2: Enabling Custom Bootloader in RAUC
The RAUC configuration was modified to use a custom backend.
File:
/etc/rauc/system.conf
Final configuration:
[system] bootloader=custom compatible=jetson-agx-orin statusfile=/var/lib/rauc/status [keyring] path=/etc/rauc/ca.cert.pem [handlers] bootloader-custom-backend=/usr/lib/rauc/backend/jetson-extlinux [slot.rootfs.0] device=/dev/mmcblk0p1 type=ext4 bootname=A [slot.rootfs.1] device=/dev/mmcblk0p2 type=ext4 bootname=B
Important:
- bootloader=custom activates the custom backend interface.
- RAUC will call the backend binary for:
- get-primary
- set-primary
- get-booted
- get-state
- set-state
Step 3: Implementing the Custom Backend Script
The backend was implemented as a simple POSIX shell script:
/usr/lib/rauc/backend/jetson-extlinux
Implementation:
#!/bin/sh
set -eu
STATE_DIR="/var/lib/rauc"
PRIMARY_FILE="${STATE_DIR}/primary"
STATE_A="${STATE_DIR}/bootstate.A"
STATE_B="${STATE_DIR}/bootstate.B"
EXTLINUX_CONF="${EXTLINUX_CONF:-/boot/extlinux/extlinux.conf}"
mkdir -p "${STATE_DIR}"
usage() {
echo "Supported commands: get-primary set-primary <A|B> get-booted get-state <A|B> set-state <A|B> <good|bad>" >&2
exit 1
}
norm_slot() {
case "${1:-}" in
A|B) echo "$1" ;;
*) usage ;;
esac
}
norm_state() {
case "${1:-}" in
good|bad) echo "$1" ;;
*) usage ;;
esac
}
get_state_file() {
case "$1" in
A) echo "$STATE_A" ;;
B) echo "$STATE_B" ;;
esac
}
slot_to_label() {
case "$1" in
A) echo "rauc-a" ;;
B) echo "rauc-b" ;;
esac
}
label_to_slot() {
case "${1:-}" in
rauc-a|rauc-A|A) echo "A" ;;
rauc-b|rauc-B|B) echo "B" ;;
*) return 1 ;;
esac
}
get_primary_from_extlinux() {
[ -f "${EXTLINUX_CONF}" ] || return 1
label="$(awk '/^[[:space:]]*DEFAULT[[:space:]]+/ {print $2; exit}' "${EXTLINUX_CONF}")"
label_to_slot "${label}"
}
set_primary_in_extlinux() {
slot="$1"
[ -f "${EXTLINUX_CONF}" ] || return 0
label="$(slot_to_label "${slot}")"
tmp="$(mktemp)"
awk -v new_default="${label}" '
BEGIN { changed = 0 }
/^[[:space:]]*DEFAULT[[:space:]]+/ {
print "DEFAULT " new_default
changed = 1
next
}
{ print }
END {
if (!changed)
print "DEFAULT " new_default
}
' "${EXTLINUX_CONF}" > "${tmp}"
cat "${tmp}" > "${EXTLINUX_CONF}"
rm -f "${tmp}"
}
cmd="${1:-}"
shift || true
case "$cmd" in
get-primary)
if get_primary_from_extlinux >/dev/null 2>&1; then
get_primary_from_extlinux
exit 0
fi
if [ -f "${PRIMARY_FILE}" ]; then
cat "${PRIMARY_FILE}"
exit 0
fi
echo "A"
exit 0
;;
set-primary)
slot="$(norm_slot "${1:-}")"
set_primary_in_extlinux "${slot}"
echo "${slot}" > "${PRIMARY_FILE}"
exit 0
;;
get-booted)
if grep -q 'rauc.slot=' /proc/cmdline 2>/dev/null; then
sed -n 's/.*rauc\.slot=\([AB]\).*/\1/p' /proc/cmdline | head -n1
exit 0
fi
get_primary_from_extlinux || echo "A"
exit 0
;;
get-state)
slot="$(norm_slot "${1:-}")"
f="$(get_state_file "${slot}")"
if [ -f "${f}" ]; then
cat "${f}"
else
echo "bad"
fi
exit 0
;;
set-state)
slot="$(norm_slot "${1:-}")"
st="$(norm_state "${2:-}")"
f="$(get_state_file "${slot}")"
echo "${st}" > "${f}"
exit 0
;;
*)
usage
;;
esac
How Slot Switching Works with This Backend
The switching process is now:
- rauc install bundle.raucb writes the new image to the inactive slot
- RAUC calls set-primary <slot>
- The backend updates the primary slot and extlinux DEFAULT
- On next reboot:
- extlinux selects the target LABEL
- kernel cmdline exposes rauc.slot
- RAUC validates the booted slot and slot state
Manual control is also possible:
/usr/lib/rauc/backend/jetson-extlinux get-primary /usr/lib/rauc/backend/jetson-extlinux set-primary A /usr/lib/rauc/backend/jetson-extlinux set-primary B
Validated Result
At the end of this page, the update flow validated was:
- Build image with RAUC
- Flash image
- Start RAUC service
- Generate signing keys
- Build signed bundle
- Install bundle
- Write inactive slot
- Switch slot
- Reboot into updated rootfs
This confirms a working signed A/B update flow using RAUC on Jetson AGX Orin.